package csvchk

import (
	"math"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"
)

// Элемент массива полей структуры данных.
type csvstr struct {
	// Описание поля из строки данных
	//  (для включения в отчёт об ошибке).
	field string
	// Функция проверки значения.
	// Функция принимает проверяемое значение (как текст),
	//  возвращает результат проверки и тип ошибки (если есть).
	check func(string) (bool, string)
}

// Разбирает строку структуры данных
// и возвращает массив полей структуры данных.
//
// Структура - строка с перечнем типов данных полей в потоке (разделитель - ";").
// Пример структуры - "i2*;b;i4;f;n(3,2);d;c(10)".
// Типы данных:
// - b (bool)      - логическое значение (true/false 1/0);
// - i (int)       - знаковое целое число 2, 4 или 8 байт;
// - r (real)      - число с плавающей точкой, 4 байта;
// - f (float)     - число с плавающей точкой, 8 байт;
// - n (numeric)   - число с произвольной точностью;
// - d (date)      - дата (yyyy-mm-dd);
// - t (timestamp) - дата время (yyyy-mm-dd hh:mm:ss);
// - c (char)      - текст фиксированной длинны;
// - v (varchar)   - текст переменной длинны.
// Размеры целочисленных типов задаются в байтах после имени: i2, i4, i8.
// Максимальный размер текста указывается в скобках: c(10), v(250).
// Для типа numeric указывается точность и масштаб: n(18,4).
// Символом "*" помечаются обязательные для заполнения поля.
//
// Типы выявляемых ошибок:
// - FRMT: нарушен формат строки данных (неправильное количество полей);
// - NULL: отсутствует значение в обязательном для заполнения поле;
// - TYPE: значение не соответствует типу данных поля;
// - SIZE: превышение максимальной длинны текста;
// - OVRF: переполнение числового значения.
func parseStucture(csvstruct string) []csvstr {
	if len(csvstruct) == 0 {
		// Если строка структуры данных пустая,
		//  то возаращается пустой массив данных
		//  (для корректной обработки пустого массива данных).
		return []csvstr{}
	}
	fields := strings.Split(strings.ToLower(csvstruct), ";")
	var csvstrs = make([]csvstr, 0, len(fields))
	for _, field := range fields {
		// Признак обязательного для заполнения поля данных (признак удаляется).
		reqr := false
		if strings.Contains(field, "*") {
			reqr = true
			field = strings.Replace(field, "*", "", -1)
		}
		// Извлечение из описания типа указания размера (если есть).
		// Для некоторых типов данных после обозначения типа
		//  указывается размер в формате "(#)" или "(#,#)", где # - число.
		field_parts := strings.Split(field, "(")
		dttp := field_parts[0]
		size := 0
		if len(field_parts) > 1 {
			sz := strings.Replace(field_parts[1], ")", "", -1)
			var err error
			if strings.Contains(sz, ",") {
				ps := strings.Split(sz, ",")
				var p, s int
				p, err = strconv.Atoi(ps[0])
				if err == nil {
					s, err = strconv.Atoi(ps[1])
				}
				size = p - s
			} else {
				size, err = strconv.Atoi(sz)
			}
			if err != nil {
				panic(err)
			}
			if size < 1 {
				panic(strconv.Itoa(size) + "?")
			}
		}
		// Выбор функции проверки значения на соответствие типу данных.
		var checkType func(string) (bool, string)
		switch dttp {
		case "b", "bool", "boolean":
			checkType = func(s string) (bool, string) {
				_, e := strconv.ParseBool(s)
				return e == nil, "TYPE"
			}
		case "i2", "int2", "i16", "int16", "smallint":
			if size == 0 {
				size = 16
			}
			fallthrough
		case "i4", "int4", "i32", "int32", "i", "int", "integer":
			if size == 0 {
				size = 32
			}
			fallthrough
		case "i8", "int8", "i64", "int64", "bigint":
			if size == 0 {
				size = 64
			}
			checkType = func(s string) (bool, string) {
				_, e := strconv.ParseInt(s, 10, size)
				return e == nil, "TYPE"
			}
		case "r", "real", "f4", "float4", "f32", "float32":
			if size == 0 {
				size = 32
			}
			fallthrough
		case "f", "float", "f8", "float8", "f64", "float64", "double":
			if size == 0 {
				size = 64
			}
			checkType = func(s string) (bool, string) {
				_, e := strconv.ParseFloat(s, size)
				return e == nil, "TYPE"
			}
		case "n", "numeric", "decimal":
			maxv := math.Pow(10, float64(size))
			checkType = func(s string) (bool, string) {
				v, e := strconv.ParseFloat(s, 64)
				if e != nil {
					return false, "TYPE"
				}
				if size > 0 && v >= maxv {
					return false, "OVRF"
				}
				return true, "TYPE"
			}
		case "d", "date", "t", "timestamp", "timestamptz":
			var tf string
			checkType = func(s string) (bool, string) {
				switch {
				case len(s) == 10:
					tf = "2006-01-02"
				case len(s) == 19:
					tf = "2006-01-02 15:04:05"
				case len(s) == 26 && s[19] == ' ':
					tf = "2006-01-02 15:04:05 -07:00"
				case len(s) == 26 && s[19] == '.':
					tf = "2006-01-02 15:04:05.000000"
				case len(s) == 33:
					tf = "2006-01-02 15:04:05.000000 -07:00"
				}
				_, e := time.Parse(tf, s)
				return e == nil, "TYPE"
			}
		case "c", "char", "character", "v", "varchar":
			if size == 0 {
				size = 1 // by default for type without size
			}
			checkType = func(s string) (bool, string) {
				return utf8.RuneCountInString(s) <= size, "SIZE"
			}
		default:
			// неизвестный тип данных - без проверки
			checkType = func(s string) (bool, string) {
				return true, ""
			}
		}
		// Функция проверки типа "заворачивается" в функцию проверки заполнения
		var check = func(s string) (bool, string) {
			// Если на проверку передано пустое значение,
			//  то проверяется обязателность заполнения поля.
			if len(s) > 0 {
				return checkType(s)
			} else {
				return !reqr, "NULL"
			}
		}
		// Формирование элемента массива полей структуры данных
		//  и добавление в общий массив структуры.
		csvstr := csvstr{
			field,
			check,
		}
		csvstrs = append(csvstrs, csvstr)
	}
	return csvstrs
}
